// 版权归Go作者所有。版权所有。
// 此源代码的使用受BSD样式的约束
// 可以在许可证文件中找到的许可证。

package logopt

import (
	"cmd/internal/obj"
	"cmd/internal/src"
	"encoding/json"
	"fmt"
	"internal/buildcfg"
	"io"
	"log"
	"net/url"
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"sync"
	"unicode"
)

// 这将为Go编译器的-json选项实现（非）优化日志记录
// 选项是-json 0，<destination>。
// None
// 0为版本号；为避免需要同步更新，如果
// 新版本的日志将出现，编译器将在一段时间内同时支持，
// 客户将指定他们需要什么。
// None
// <destination>是一个目录。
// 使用前导/或os.PathSeparator指定目录，
// 或更明确地使用文件：
// 处理窗口上的角盒，并允许指定相对位置
// 目录路径（这通常是个坏主意，因为本地目录
// 在构建中变化很大，特别是在模块和/或供应商方面，并且可能
// 不可写）。
// None
// 对于编译的每个包pkg，都有一个url.PathEscape（pkg）命名的子目录
// 是创建的。对于生成
// 诊断（无诊断意味着无文件），
// 将创建一个url.PathEscape（文件）+“.json”-命名文件，其中包含
// 记录诊断。
// None
// 例如，“cmd%2Finternal%2Fdwarf/%3Cautogenerated%3E.json”
// 对于“cmd/internal/dwarf”和<autogenerated>（这实际上不是一个文件，但编译器可以看到）
// None
// 如果包字符串为空，则在内部用编码为%00的字符串（0）替换。
// None
// 每个日志文件都以标识版本的JSON记录开始，
// 平台等上下文，然后进行相关优化
// LSP诊断记录，每行一条（LSP版本3.15，此处使用的子集与3.14没有差异
// 请参阅https:
// None
// 诊断的字段按以下方式使用：
// 范围：最外面的震源位置，现在开始和结束是相等的。
// 严重性：（始终）严重性信息（3）
// 来源：（总是）“go编译器”
// 代码：描述遗漏优化的字符串，例如，“nilcheck”、“cannotInline”、“isInBounds”、“escape”
// 消息：根据代码，附加信息，例如函数无法内联的原因。
// 相关信息：如果错过的优化实际上发生在范围内联的函数上，
// 然后内联位置的顺序出现在这里，从（第二）最外层到最内层，
// 每个都带有消息=“inlineLoc”。
// None
// 对于逃逸分析解释，在任何外部内联位置之后，
// 解释行出现了，每行后面都可能有自己的内联
// 发生在内联函数中的逃逸流的位置。
// None
// 例如<destination>/cmd%2fcomile%2Finternal%2Fssa/prove.json
// 可以从以下行开始（为便于阅读而包装）：
// None
// {“version”：0，“package”：“cmd/compile/internal/ssa”，“goos”：“darwin”，“goarch”：“amd64”，
// “gc_版本”：“devel+e1b9a57852 2019年11月1日星期五15:07:00-0400”，
// “文件”：“/Users/drchase/work/go/src/cmd/compile/internal/ssa/prove.go”}
// None
// 以及随后包含的内容（也包裹起来以便于阅读）：
// None
// {“范围”：{“开始”：{“行”：191，“字符”：24}，“结束”：{“行”：191，“字符”：24}，
// “严重性”：3，“代码”：“nilcheck”，“源”：“go编译器”，“消息”：“，
// “相关信息”：[
// {“位置”：{“uri”：“文件：
// “范围”：{“开始”：{“行”：153，“字符”：16}，“结束”：{“行”：153，“字符”：16}}，
// “消息”：“inlineLoc”}]}
// None
// 也就是说，在prove.go（从上下文中隐式显示，在文件名和标题行中提供），
// 第191行第24列，生成的代码中发生了nilcheck。
// 相关信息表明此代码实际上来自
// 对func.go的内联调用，第153行，字符16。
// None
// 证明。go:191：
// ft.orderS=f.newPoset（）
// 职能go:152和153：
// func（f*func）newPoset（）*poset{
// 如果len（f.Cache.scrPoset）>0{
// None
// 例如，在包为空的情况下，头记录中也会使用字符串（0）包名
// None
// go工具编译-json=0，文件：
// head-1 logopt/%00/x.json
// {“版本”：0，“包”：“\u0000”，“goos”：“darwin”，“goarch”：“amd64”，“gc_版本”：“devel+86487adf6a-Thu Nov 7 19:34:56 2019-0500”，“文件”：“x.go”}

type VersionHeader struct {
	Version   int    `json:"version"`
	Package   string `json:"package"`
	Goos      string `json:"goos"`
	Goarch    string `json:"goarch"`
	GcVersion string `json:"gc_version"`
	File      string `json:"file,omitempty"` // LSP需要一个封闭的资源，即文件
}

// DocumentURI、位置、范围、位置、诊断、诊断相关信息都重用gopls中的json定义。
// 请参阅https:

type DocumentURI string

type Position struct {
	Line      uint `json:"line"`      // gopls使用float64，但整数的json输出是相同的
	Character uint `json:"character"` // gopls使用float64，但整数的json输出是相同的
}

// 文本文档中表示为（从零开始）开始和结束位置的范围。
// 范围与编辑器中的选择相当。因此，结束位置是排他性的。
// 如果要指定包含行（包括行尾字符）的范围
// 然后使用结束位置表示下一行的开始。
type Range struct {
	/*Start defined:
	 * The range's start position
	 */
	Start Position `json:"start"`

	/*End defined:
	 * The range's end position
	 */
	End Position `json:"end"` // 独家
}

// 位置表示资源中的位置，例如文本文件中的行。
type Location struct {
	// URI是
	URI DocumentURI `json:"uri"`

	// 范围是
	Range Range `json:"range"`
}

/* DiagnosticRelatedInformation defined:
 * Represents a related message and source code location for a diagnostic. This should be
 * used to point to code locations that cause or related to a diagnostics, e.g when duplicating
 * a symbol in a scope.
 */
type DiagnosticRelatedInformation struct {

	/*Location defined:
	 * The location of this related diagnostic information.
	 */
	Location Location `json:"location"`

	/*Message defined:
	 * The message of this related diagnostic information.
	 */
	Message string `json:"message"`
}

// DiagnosticSeverity定义常量
type DiagnosticSeverity uint

const (
	/*SeverityInformation defined:
	 * Reports an information.
	 */
	SeverityInformation DiagnosticSeverity = 3
)

// DiagnosticTag定义常量
type DiagnosticTag uint

/*Diagnostic defined:
 * Represents a diagnostic, such as a compiler error or warning. Diagnostic objects
 * are only valid in the scope of a resource.
 */
type Diagnostic struct {

	/*Range defined:
	 * The range at which the message applies
	 */
	Range Range `json:"range"`

	/*Severity defined:
	 * The diagnostic's severity. Can be omitted. If omitted it is up to the
	 * client to interpret diagnostics as error, warning, info or hint.
	 */
	Severity DiagnosticSeverity `json:"severity,omitempty"` // 始终为优化器日志记录提供严格的信息。

	/*Code defined:
	 * The diagnostic's code, which usually appear in the user interface.
	 */
	Code string `json:"code,omitempty"` // LSP使用'number | string'=gopls接口{}，但此处仅使用字符串，例如“boundsCheck”、“nilcheck”等。

	/*Source defined:
	 * A human-readable string describing the source of this
	 * diagnostic, e.g. 'typescript' or 'super lint'. It usually
	 * appears in the user interface.
	 */
	Source string `json:"source,omitempty"` // “go编译器”

	/*Message defined:
	 * The diagnostic's message. It usually appears in the user interface
	 */
	Message string `json:"message"` // 有时使用，提供附加信息。

	/*Tags defined:
	 * Additional metadata about the diagnostic.
	 */
	Tags []DiagnosticTag `json:"tags,omitempty"` // 日志优化总是空的。

	/*RelatedInformation defined:
	 * An array of related diagnostic information, e.g. when symbol-names within
	 * a scope collide all definitions can be marked via this property.
	 */
	RelatedInformation []DiagnosticRelatedInformation `json:"relatedInformation,omitempty"`
}

// LoggedOpt是编译器产生和积累的数据，
// 转换为JSON供人或IDE使用。
type LoggedOpt struct {
	pos          src.XPos      // 事件发生时的源代码位置。如果它是内联的，那么外部和所有内联位置都将显示在JSON中。
	compilerPass string        // 编译器通过。供人/临时消费；未在JSON中显示（尚未显示）
	functionName string        // 函数名。供人/临时消费；未在JSON中显示（尚未显示）
	what         string        // （非）优化；“nilcheck”、“boundsCheck”、“inline”、“noInline”
	target       []interface{} // “what”的可选目标或参数——什么是内联的、为什么不是内联的、副本大小等。第一个是最重要的/相关的。
}

type logFormat uint8

const (
	None  logFormat = iota
	Json0           // LSP 3.14、3.15版本为0；LSP的未来版本可能会更改格式，并且在更新客户端时，编译器可能需要同时支持这两种格式。
)

var Format = None
var dest string

// LogJsonOption解析并验证附加到-json编译器标志的版本、目录值。
func LogJsonOption(flagValue string) {
	version, directory := parseLogFlag("json", flagValue)
	if version != 0 {
		log.Fatal("-json version must be 0")
	}
	dest = checkLogPath(directory)
	Format = Json0
}

// parseLogFlag检查传递给-json的标志
// 对于版本，使用目标格式并返回两部分。
func parseLogFlag(flag, value string) (version int, directory string) {
	if Format != None {
		log.Fatal("Cannot repeat -json flag")
	}
	commaAt := strings.Index(value, ",")
	if commaAt <= 0 {
		log.Fatalf("-%s option should be '<version>,<destination>' where <version> is a number", flag)
	}
	v, err := strconv.Atoi(value[:commaAt])
	if err != nil {
		log.Fatalf("-%s option should be '<version>,<destination>' where <version> is a number: err=%v", flag, err)
	}
	version = v
	directory = value[commaAt+1:]
	return
}

// 如果文件URI的格式与
// Windows URI。Parse包不专门处理Windows路径
// （参见golang/go#6027），因此我们检查URI路径是否有驱动器前缀（例如“/C:”）。
// （复制自tools/internal/span/uri.go）
// 与Windows上filepath.isbs中的处理相比，这不太全面。
func isWindowsDriveURIPath(uri string) bool {
	if len(uri) < 4 {
		return false
	}
	return uri[0] == '/' && unicode.IsLetter(rune(uri[1])) && uri[2] == ':'
}

func parseLogPath(destination string) (string, string) {
	if filepath.IsAbs(destination) {
		return filepath.Clean(destination), ""
	}
	if strings.HasPrefix(destination, "file:// ") {
		uri, err := url.Parse(destination)
		if err != nil {
			return "", fmt.Sprintf("optimizer logging destination looked like file:// URI但无法分析：错误=%v“，错误）
		}
		destination = uri.Host + uri.Path
		if isWindowsDriveURIPath(destination) {
			// 带材引出/引出/C：
			// 与tools/internal/span/uri.go不同，不要将驱动器号大写——让filepath.Clean执行它的功能。
			destination = destination[1:]
		}
		return filepath.Clean(destination), ""
	}
	return "", fmt.Sprintf("optimizer logging destination %s was neither %s-prefixed directory nor file:// -前缀文件URI”，目标，字符串（filepath.Separator））
}

// checkLogPath对指定
// 优化器日志记录指向的目录，如果
// 它通过测试，将字符串存储在lou dir中
func checkLogPath(destination string) string {
	path, complaint := parseLogPath(destination)
	if complaint != "" {
		log.Fatalf(complaint)
	}
	err := os.MkdirAll(path, 0755)
	if err != nil {
		log.Fatalf("optimizer logging destination '<version>,<directory>' but could not create <directory>: err=%v", err)
	}
	return path
}

var loggedOpts []*LoggedOpt
var mu = sync.Mutex{} // mu保护原木。

// NewLoggedOpt分配一个新的LoggedOpt，稍后作为“args”传递给NewLoggedOpt或LogOpt。
// Pos是源位置（包括内联），消息是什么，pass是创建消息的过程，
// funcName是函数的名称
// 这种方法的典型用途是为遗漏的优化积累一个解释，例如，为什么有什么东西漏掉了？
func NewLoggedOpt(pos src.XPos, what, pass, funcName string, args ...interface{}) *LoggedOpt {
	pass = strings.Replace(pass, " ", "_", -1)
	return &LoggedOpt{pos, pass, funcName, what, args}
}

// Logopt记录有关编译器执行的优化（通常遗漏）的信息。
// Pos是源位置（包括内联），消息是什么，pass是创建消息的过程，
// funcName是函数的名称
func LogOpt(pos src.XPos, what, pass, funcName string, args ...interface{}) {
	if Format == None {
		return
	}
	lo := NewLoggedOpt(pos, what, pass, funcName, args...)
	mu.Lock()
	defer mu.Unlock()
	// 由于来自后端的并发调用，不知道订单将是什么，但在使用前，通过外部Pos进行稳定排序。
	loggedOpts = append(loggedOpts, lo)
}

// Enabled返回是否启用了优化日志记录。
func Enabled() bool {
	switch Format {
	case None:
		return false
	case Json0:
		return true
	}
	panic("Unexpected optimizer-logging level")
}

// byPos按源位置对诊断进行排序。
type byPos struct {
	ctxt *obj.Link
	a    []*LoggedOpt
}

func (x byPos) Len() int { return len(x.a) }
func (x byPos) Less(i, j int) bool {
	return x.ctxt.OutermostPos(x.a[i].pos).Before(x.ctxt.OutermostPos(x.a[j].pos))
}
func (x byPos) Swap(i, j int) { x.a[i], x.a[j] = x.a[j], x.a[i] }

func writerForLSP(subdirpath, file string) io.WriteCloser {
	basename := file
	lastslash := strings.LastIndexAny(basename, "\\/")
	if lastslash != -1 {
		basename = basename[lastslash+1:]
	}
	lastdot := strings.LastIndex(basename, ".go")
	if lastdot != -1 {
		basename = basename[:lastdot]
	}
	basename = pathEscape(basename)

	// 假设一个目录，生成一个文件
	p := filepath.Join(subdirpath, basename+".json")
	w, err := os.Create(p)
	if err != nil {
		log.Fatalf("Could not create file %s for logging optimizer actions, %v", p, err)
	}
	return w
}

func fixSlash(f string) string {
	if os.PathSeparator == '/' {
		return f
	}
	return strings.Replace(f, string(os.PathSeparator), "/", -1)
}

func uriIfy(f string) DocumentURI {
	url := url.URL{
		Scheme: "file",
		Path:   fixSlash(f),
	}
	return DocumentURI(url.String())
}

// 返回filename，将第一次出现的$GOROOT替换为
// GOROOT的实际值（因为LSP不说“$GOROOT”）。
func uprootedPath(filename string) string {
	if !strings.HasPrefix(filename, "$GOROOT/") {
		return filename
	}
	return buildcfg.GOROOT + filename[len("$GOROOT"):]
}

// FlushLoggedOpts刷新所有累积的优化日志项。
func FlushLoggedOpts(ctxt *obj.Link, slashPkgPath string) {
	if Format == None {
		return
	}

	sort.Stable(byPos{ctxt, loggedOpts}) // 稳定是保持每个函数顺序所必需的，它是可重复的。
	switch Format {

	case Json0: // LSP 3.15
		var posTmp []src.Pos
		var encoder *json.Encoder
		var w io.WriteCloser

		if slashPkgPath == "" {
			slashPkgPath = "\000"
		}
		subdirpath := filepath.Join(dest, pathEscape(slashPkgPath))
		err := os.MkdirAll(subdirpath, 0755)
		if err != nil {
			log.Fatalf("Could not create directory %s for logging optimizer actions, %v", subdirpath, err)
		}
		diagnostic := Diagnostic{Source: "go compiler", Severity: SeverityInformation}

		// 对于LSP，为包创建一个子目录，对于每个文件foo.go，在该子目录中创建foo.json。
		currentFile := ""
		for _, x := range loggedOpts {
			posTmp, p0 := x.parsePos(ctxt, posTmp)
			p0f := uprootedPath(p0.Filename())

			if currentFile != p0f {
				if w != nil {
					w.Close()
				}
				currentFile = p0f
				w = writerForLSP(subdirpath, currentFile)
				encoder = json.NewEncoder(w)
				encoder.Encode(VersionHeader{Version: 0, Package: slashPkgPath, Goos: buildcfg.GOOS, Goarch: buildcfg.GOARCH, GcVersion: buildcfg.Version, File: currentFile})
			}

			// 第一个“目标”是最重要的。
			var target string
			if len(x.target) > 0 {
				target = fmt.Sprint(x.target[0])
			}

			diagnostic.Code = x.what
			diagnostic.Message = target
			diagnostic.Range = newPointRange(p0)
			diagnostic.RelatedInformation = diagnostic.RelatedInformation[:0]

			appendInlinedPos(posTmp, &diagnostic)

			// 诊断说明在内联信息之后存储在RelatedInformation中
			if len(x.target) > 1 {
				switch y := x.target[1].(type) {
				case []*LoggedOpt:
					for _, z := range y {
						posTmp, p0 := z.parsePos(ctxt, posTmp)
						loc := newLocation(p0)
						msg := z.what
						if len(z.target) > 0 {
							msg = msg + ": " + fmt.Sprint(z.target[0])
						}

						diagnostic.RelatedInformation = append(diagnostic.RelatedInformation, DiagnosticRelatedInformation{Location: loc, Message: msg})
						appendInlinedPos(posTmp, &diagnostic)
					}
				}
			}

			encoder.Encode(diagnostic)
		}
		if w != nil {
			w.Close()
		}
	}
}

// newPointRange返回编译器源位置p的单个位置范围。
func newPointRange(p src.Pos) Range {
	return Range{Start: Position{p.Line(), p.Col()},
		End: Position{p.Line(), p.Col()}}
}

// newLocation返回编译器源位置p的位置
func newLocation(p src.Pos) Location {
	loc := Location{URI: uriIfy(uprootedPath(p.Filename())), Range: newPointRange(p)}
	return loc
}

// appendInlinedPos从POSTP提取内联信息并将其附加到诊断
func appendInlinedPos(posTmp []src.Pos, diagnostic *Diagnostic) {
	for i := 1; i < len(posTmp); i++ {
		p := posTmp[i]
		loc := newLocation(p)
		diagnostic.RelatedInformation = append(diagnostic.RelatedInformation, DiagnosticRelatedInformation{Location: loc, Message: "inlineLoc"})
	}
}

func (x *LoggedOpt) parsePos(ctxt *obj.Link, posTmp []src.Pos) ([]src.Pos, src.Pos) {
	posTmp = ctxt.AllPos(x.pos, posTmp)
	// 反向邮戳，将最外层放在第一位。
	l := len(posTmp)
	for i := 0; i < l/2; i++ {
		posTmp[i], posTmp[l-i-1] = posTmp[l-i-1], posTmp[i]
	}
	p0 := posTmp[0]
	return posTmp, p0
}
